package com.bensonbasement.snorflux.server;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.apache.mina.core.session.IoSession;

import com.bensonbasement.snorflux.commands.Command;
import com.bensonbasement.snorflux.messages.fromclient.ChatMessage;
import com.bensonbasement.snorflux.messages.fromclient.CommandResponseMessage;
import com.bensonbasement.snorflux.messages.fromclient.IFromClientMessage;
import com.bensonbasement.snorflux.messages.fromclient.JoinMessage;
import com.bensonbasement.snorflux.messages.fromserver.ChatCastMessage;
import com.bensonbasement.snorflux.messages.fromserver.IFromServerMessage;
import com.bensonbasement.snorflux.messages.fromserver.NewPlayerMessage;
import com.bensonbasement.snorflux.messages.fromserver.PlayerInfo;
import com.bensonbasement.snorflux.messages.fromserver.PlayerStatus;
import com.bensonbasement.snorflux.messages.fromserver.PlayerStatusChangeMessage;
import com.bensonbasement.snorflux.messages.fromserver.ReportMessage;
import com.bensonbasement.snorflux.messages.fromserver.ServerInfoMessage;
import com.bensonbasement.snorflux.messages.fromserver.reports.Report;
import com.bensonbasement.snorflux.server.verbs.Verb;
import com.bensonbasement.snorflux.server.verbs.VerbExecuteCanceledException;
import com.bensonbasement.snorflux.server.world.AIBrain;
import com.bensonbasement.snorflux.server.world.Actor;
import com.bensonbasement.snorflux.server.world.BigEntity;
import com.bensonbasement.snorflux.server.world.Brain;
import com.bensonbasement.snorflux.server.world.BrainTeam;
import com.bensonbasement.snorflux.server.world.Entity;
import com.bensonbasement.snorflux.server.world.Level;
import com.bensonbasement.snorflux.server.world.PlayerBrain;
import com.bensonbasement.snorflux.server.world.SmallEntity;
import com.bensonbasement.snorflux.server.world.Space;
import com.bensonbasement.snorflux.server.world.World;
import com.bensonbasement.snorflux.server.world.prototypes.ActorPrototype;
import com.bensonbasement.snorflux.server.world.prototypes.BigEntityPrototype;
import com.bensonbasement.snorflux.server.world.prototypes.SmallEntityPrototype;
import com.bensonbasement.snorflux.server.world.prototypes.SpacePrototype;
import com.bensonbasement.snorflux.utils.Coords;

public class Game implements Serializable {
	
	// TODO: Do something more appropriate with this:
	public static final Random rng = new Random();
	
	private boolean started = false;
	private transient boolean running = false;
	
	private HashSet<Actor> alreadyMovedThisTurn = null;
	private Level currentLevel = null;
	private Actor currentActor = null;
	private World world;
	private final Set<PlayerBrain> playerBrains;
	private final BrainTeam sharedSightTeam;
	
	public Game(boolean sharedSight) {
		playerBrains = new HashSet<PlayerBrain>();
		if (sharedSight) {
			sharedSightTeam = new BrainTeam();
		}
		else {
			sharedSightTeam = null;
		}
	}

	public void handleMessage(IoSession session, IFromClientMessage message) {

		// May return null (which is fine):
		PlayerBrain fromPlayer = this.findBrainFromSession(session);
		
		// TODO: Clean this up?
		if (message instanceof JoinMessage) {
			handleJoinMessage(session, (JoinMessage) message);
		}
		else if (message instanceof ChatMessage) {
			handleChatMessage(fromPlayer, (ChatMessage) message);
		}
		else if (message instanceof CommandResponseMessage) {
			handleCommandResponseMessage(fromPlayer, (CommandResponseMessage) message);
		}
	}

	/**
	 * This method is called when a player attempts to
	 * join or rejoin.
	 * @param player
	 * @return the player's name, or null if the player is rejected
	 */
	public void handleJoinMessage(IoSession session, JoinMessage message) {
		// TODO: Make sure the session isn't already associated with a player
		String requestedName = message.getPlayerName();
		PlayerBrain brain = findBrainFromName(requestedName);
		if (brain == null && !started) {
			createPlayer(requestedName, session);
		}
		else if (brain == null && started) {
			// Rejected - the game is already started.
			// TODO: Send rejected message.
		}
		else if (brain != null && !brain.hasSession() && started){
			// Reconnect existing player
			broadcastMessage(new PlayerStatusChangeMessage(brain.getPlayerName(),PlayerStatus.CONNECTED));
			brain.joinToSession(session);
			brain.sendJoinedMessage(generateAllPlayersArray());
			if (running) {
				// Send the player a message letting them know which space to show first...
				// HACK: This assumes they only have one controlled actor.
				Actor actor = brain.getControlledActors().get(0);
				Report report = new Report(actor.getSpace().getReference());
				ReportMessage reportMessage = new ReportMessage(report);
				brain.sendMessage(reportMessage);
				if (currentActor.getBrain() == brain) {
					brain.sendCommandRequest(currentActor);
				}
			}
		}
		else if (brain != null && brain.hasSession()){
			// There's already a player with that name, so change the name.
			int i = 2;
			String newName;
			while (true) {
				newName = requestedName+i;
				PlayerBrain newNameBrain = findBrainFromName(newName);
				if (newNameBrain == null) {
					break;
				}
				i++;
			}
			createPlayer(newName,session);
		}
		else {
			throw new RuntimeException("Should NOT have gotten here.");
		}
	}
	
	private void createPlayer(String playerName, IoSession session) {
		// Create new player
		BrainTeam team;
		if (sharedSightTeam == null) {
			team = new BrainTeam();
		}
		else {
			team = sharedSightTeam;
		}
		PlayerBrain brain = new PlayerBrain(playerName,team);
		NewPlayerMessage newPlayerMessage = new NewPlayerMessage(brain.getPlayerName());
		this.broadcastMessage(newPlayerMessage);
		playerBrains.add(brain);
		brain.joinToSession(session);
		brain.sendJoinedMessage(generateAllPlayersArray());
	}
	
	private PlayerInfo[] generateAllPlayersArray() {
		PlayerInfo[] allPlayers = new PlayerInfo[playerBrains.size()];
		int i = 0;
		for (PlayerBrain player : playerBrains) {
			PlayerStatus status;
			if (player.hasSession()) {
				status = PlayerStatus.CONNECTED;
			}
			else {
				status = PlayerStatus.DISCONNECTED;
			}
			allPlayers[i] = new PlayerInfo(player.getPlayerName(),status);
			i++;
		}
		return allPlayers;
	}
	
	public void playerDisconnected(IoSession session) {
		PlayerBrain brain = findBrainFromSession(session);
		if (brain != null) {
			brain.disconnectAndClearSession();
			PlayerStatus newStatus;
			if (!started) {
				playerBrains.remove(brain);
				newStatus = PlayerStatus.QUIT;
			}
			else {
				brain.disconnectAndClearSession();
				newStatus = PlayerStatus.DISCONNECTED;
			}
			PlayerStatusChangeMessage message = new PlayerStatusChangeMessage(brain.getPlayerName(),newStatus);
			broadcastMessage(message);
		}
	}
	
	public void broadcastMessage(IFromServerMessage message) {
		for (PlayerBrain player : playerBrains) {
			// TODO: Check if the brain is connected?
			player.sendMessage(message);
		}
	}
	
	public void broadcastServerInfoMessage(String messageText) {
		ServerInfoMessage message = new ServerInfoMessage(messageText);
		this.broadcastMessage(message);
		System.out.println(messageText);
	}
	
	public void handleChatMessage(PlayerBrain fromPlayer, ChatMessage message) {
		ChatCastMessage chatCast = new ChatCastMessage(fromPlayer.getPlayerName(),message.getChatText());
		this.broadcastMessage(chatCast);
	}
	
	public void handleCommandResponseMessage(PlayerBrain fromPlayer, CommandResponseMessage message) {
		// TODO: Verify that the command is actually from the correct player.
		if (fromPlayer == currentActor.getBrain()) {
			handlePlayerCommand(message.getCommand());
		}
		else {
			throw new RuntimeException("Command received from wrong player!");
		}
	}
	
	public void disconnectAllPlayers() {
		for (PlayerBrain player : playerBrains) {
			player.getSession().close(false);
		}
	}
	
//	public void continueGame(List<PlayerInfo> players) {
//		for (PlayerInfo player : players) {
//			PlayerBrain brain = findBrainForPlayer(player);
//			brain.setSession(player.getSession());
//		}
//	}
	
	private PlayerBrain findBrainFromName(String playerName) {
		// Try to find a matching brain.
		for (PlayerBrain brain : playerBrains) {
			if (brain.getPlayerName().equals(playerName)) {
				return brain;
			}
		}
		return null; // If not found.
	}
	
	private PlayerBrain findBrainFromSession(IoSession session) {
		// Try to find a matching brain.
		for (PlayerBrain brain : playerBrains) {
			if (brain.getSession() == session) {
				return brain;
			}
		}
		return null; // If not found.
	}
	
	public void startGame() {
		if (!started) {
			setupTestGame();
			
			this.alreadyMovedThisTurn = new HashSet<Actor>();
			this.world.startTurn();
			this.currentLevel = this.world.nextLevel();
			this.currentLevel.startTurn();
			this.currentActor = this.currentLevel.nextActor();
			this.currentActor.startTurn();
			started = true;
		}
		
		// Send each player a message indicating what to show on their screen.
		for (PlayerBrain player : playerBrains) {
			// HACK: This assumes they only have one controlled actor.
			Actor actor = player.getControlledActors().get(0);
			Report report = new Report(actor.getSpace().getReference());
			ReportMessage message = new ReportMessage(report);
			player.sendMessage(message);
		}
		running = true;
		this.runUntilCurrent();
	}
	
	public void saveGame(File file) {
		FileOutputStream fileOut = null;
		ObjectOutputStream objectOut = null;
		try
		{
			fileOut = new FileOutputStream(file);
			objectOut = new ObjectOutputStream(fileOut);
			objectOut.writeObject(this);
			objectOut.close();
		}
		catch(IOException ex)
		{
			ex.printStackTrace();
		}
		// TODO: End the game.
	}
	
	public boolean isStarted() {
		return started;
	}
	
	public boolean isRunning() {
		return running;
	}

	private void handlePlayerCommand(Command command) {
		this.executeCommand(command);
		this.runUntilCurrent();
	}
	
	private void runUntilCurrent() {
		boolean done = false;
		while ( !done ) {
			Brain currentBrain = this.currentActor.getBrain();
			if (currentBrain instanceof PlayerBrain) {
				PlayerBrain playerBrain = (PlayerBrain) currentBrain;
				playerBrain.sendCommandRequest(this.currentActor);
				done = true;
			}
			else if (currentBrain instanceof AIBrain) {
				AIBrain aiBrain = (AIBrain) currentBrain;
				Command command = aiBrain.getCommand(this.currentActor);
				this.executeCommand(command);
			}
		}
	}
	
	private void executeCommand(Command command) {
		if (command == null) {
			this.cycleActors();
		}
		else {
			// Find the object.
			Entity object;
			if (command.getObjectID() == null) {
				object = null;
			}
			else {
				object = (Entity) this.world.getWorldElement(command.getObjectID());
			}
			
			Verb verb = world.findVerb(object, command.getVerbName());
			try {
				verb.execute(this.currentActor, object, command.getTargets());
			}
			catch (VerbExecuteCanceledException ex) {
				// TODO: Do something better here.
				// For now, ignore.
			}
		}
	}
	
	private void cycleActors() {
		// End the actor turn and start the next.
		this.currentActor = this.currentLevel.nextActor();
		if (this.currentActor == null) {
			this.currentLevel = this.world.nextLevel();
			if (this.currentLevel == null) {
				this.alreadyMovedThisTurn.clear();
				this.world.startTurn();
				this.currentLevel = this.world.nextLevel();
			}
			this.currentLevel.startTurn();
			this.currentActor = this.currentLevel.nextActor();
		}
		if (this.alreadyMovedThisTurn.contains(this.currentActor)) {
			this.cycleActors(); // RECURSIVE CALL.
		}
		else {
			this.alreadyMovedThisTurn.add(this.currentActor);
			this.currentActor.startTurn();
		}
	}
	
	
	private void setupTestGame() {
		
		ContentRepository repo = ContentRepository.getInstance();
		
		ActorPrototype playerPrototype = (ActorPrototype)repo.getPrototype("test_player");
		BigEntityPrototype wallPrototype = (BigEntityPrototype)repo.getPrototype("test_wall");
		SpacePrototype spacePrototype = (SpacePrototype)repo.getPrototype("test_floor");
		SmallEntityPrototype raygunPrototype = (SmallEntityPrototype)repo.getPrototype("test_raygun");
		SmallEntityPrototype mopPrototype = (SmallEntityPrototype)repo.getPrototype("test_mop");
		
		this.world = new World();
		
		Level testLevel = new Level(this.world, 1);
		this.world.addLevel(testLevel);
		
		List<Space> emptySpaces = new ArrayList<Space>();
		
		for (int x = 0; x < 10; x++) {
			for (int y = 0; y < 5; y++) {
				Space floor = spacePrototype.instantiate(this.world,testLevel,new Coords(x,y));
				testLevel.addSpace(floor);
				if (x == 0 || x == 9 || y == 0 || y == 4 || (x == 5 && y != 3)) {
					BigEntity wall = wallPrototype.instantiate(this.world);
					wall.setLocation(floor);
				}
				else if ((x == 1 && y == 2) ||
						(x == 4 && y == 3)) {
					SmallEntity raygun = raygunPrototype.instantiate(this.world);
					raygun.setLocation(floor);
					emptySpaces.add(floor);
				}
				else {
					emptySpaces.add(floor);
				}
			}
		}
		
		for (PlayerBrain player : playerBrains) {
			Actor playerActor = playerPrototype.instantiate(this.world);
			playerActor.setName(player.getPlayerName());
			playerActor.setBrain(player);
			SmallEntity mop = mopPrototype.instantiate(world);
			mop.setLocation(playerActor);
			Space location = emptySpaces.get(rng.nextInt(emptySpaces.size()));
			playerActor.setLocation(location);
			emptySpaces.remove(location);
		}
		testLevel.animateAll();
	}
	
	/**
	 * Called when the object is deserialized.
	 */
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		running = false;
	}
}
